"""BLE Connection Manager for handling multiple concurrent connections.

Provides connection pooling, session management, automatic reconnection,
and connection monitoring for BLE GATT server.
"""

import asyncio
import logging
import time
import uuid
import weakref
import random
import gc
from enum import Enum
from typing import Dict, List, Optional, Callable, Set, Any, Tuple
from dataclasses import dataclass, field
from collections import defaultdict, deque

logger = logging.getLogger(__name__)


# Configuration constants
MAX_CONNECTIONS = 8
CONNECTION_TIMEOUT = 300  # 5 minutes
HEARTBEAT_INTERVAL = 30   # 30 seconds
HEARTBEAT_TIMEOUT = 10    # 10 seconds
RECONNECT_ATTEMPTS = 3
RECONNECT_DELAY = 5       # 5 seconds
SESSION_TIMEOUT = 1800    # 30 minutes
CLEANUP_INTERVAL = 60     # 1 minute
MAX_IDLE_TIME = 300       # 5 minutes


class ConnectionState(Enum):
    """Connection state enumeration."""
    DISCONNECTED = 0
    CONNECTING = 1
    CONNECTED = 2
    AUTHENTICATED = 3
    ACTIVE = 4
    IDLE = 5
    RECONNECTING = 6
    ERROR = 7
    TERMINATED = 8


class ConnectionPriority(Enum):
    """Connection priority levels."""
    LOW = 0
    NORMAL = 1
    HIGH = 2
    CRITICAL = 3


class ReconnectStrategy(Enum):
    """Reconnection strategies."""
    NONE = 0
    IMMEDIATE = 1
    LINEAR_BACKOFF = 2
    EXPONENTIAL_BACKOFF = 3


@dataclass
class ConnectionMetrics:
    """Connection performance metrics."""
    active_connections: int = 0
    total_connections: int = 0
    successful_connections: int = 0
    failed_connections: int = 0
    reconnection_attempts: int = 0
    successful_reconnections: int = 0
    data_sent: int = 0
    data_received: int = 0
    average_response_time: float = 0.0
    connection_errors: int = 0
    heartbeat_failures: int = 0
    sessions_created: int = 0
    sessions_expired: int = 0
    
    @property
    def connection_success_rate(self) -> float:
        """Calculate connection success rate."""
        if self.total_connections == 0:
            return 1.0
        return self.successful_connections / self.total_connections
    
    @property
    def reconnection_success_rate(self) -> float:
        """Calculate reconnection success rate."""
        if self.reconnection_attempts == 0:
            return 1.0
        return self.successful_reconnections / self.reconnection_attempts


@dataclass  
class Session:
    """Connection session data."""
    session_id: str
    connection_id: str
    user_id: Optional[str] = None
    created_time: float = field(default_factory=time.time)
    last_access_time: float = field(default_factory=time.time)
    expires_at: float = field(default_factory=lambda: time.time() + SESSION_TIMEOUT)
    data: Dict[str, Any] = field(default_factory=dict)
    is_authenticated: bool = False
    
    @property
    def is_expired(self) -> bool:
        """Check if session has expired."""
        return time.time() > self.expires_at
    
    @property
    def idle_time(self) -> float:
        """Get session idle time in seconds."""
        return time.time() - self.last_access_time
    
    def touch(self):
        """Update last access time."""
        self.last_access_time = time.time()
        self.expires_at = time.time() + SESSION_TIMEOUT


class Connection:
    """Individual BLE connection representation."""
    
    def __init__(self, connection_id: str, device_id: str, 
                 client_address: str, priority: ConnectionPriority = ConnectionPriority.NORMAL):
        """Initialize connection.
        
        Args:
            connection_id: Unique connection identifier
            device_id: BLE device identifier  
            client_address: Client MAC address
            priority: Connection priority level
        """
        self.connection_id = connection_id
        self.device_id = device_id
        self.client_address = client_address
        self.priority = priority
        
        self.state = ConnectionState.CONNECTING
        self.created_time = time.time()
        self.connected_time: Optional[float] = None
        self.last_activity_time = time.time()
        self.last_heartbeat_time = 0.0
        
        # Connection statistics
        self.bytes_sent = 0
        self.bytes_received = 0
        self.messages_sent = 0
        self.messages_received = 0
        self.errors_count = 0
        
        # Reconnection data
        self.reconnect_attempts = 0
        self.last_reconnect_time = 0.0
        self.reconnect_strategy = ReconnectStrategy.EXPONENTIAL_BACKOFF
        
        # Session reference
        self.session: Optional[Session] = None
        
        # Callbacks
        self.on_disconnect: Optional[Callable] = None
        self.on_heartbeat_failure: Optional[Callable] = None
        self.on_error: Optional[Callable] = None
        
        logger.info(f"Connection created: {connection_id} from {client_address}")
    
    def update_activity(self):
        """Update last activity timestamp."""
        self.last_activity_time = time.time()
    
    def update_heartbeat(self):
        """Update last heartbeat timestamp.""" 
        self.last_heartbeat_time = time.time()
        self.update_activity()
    
    @property
    def idle_time(self) -> float:
        """Get connection idle time in seconds."""
        return time.time() - self.last_activity_time
    
    @property
    def is_idle(self) -> bool:
        """Check if connection is idle."""
        return self.idle_time > MAX_IDLE_TIME
    
    @property
    def heartbeat_age(self) -> float:
        """Get time since last heartbeat."""
        return time.time() - self.last_heartbeat_time
    
    @property
    def needs_heartbeat(self) -> bool:
        """Check if heartbeat is needed."""
        return self.heartbeat_age > HEARTBEAT_INTERVAL
    
    @property
    def heartbeat_failed(self) -> bool:
        """Check if heartbeat has failed."""
        return self.heartbeat_age > HEARTBEAT_TIMEOUT and self.last_heartbeat_time > 0
    
    def get_info(self) -> Dict[str, Any]:
        """Get connection information."""
        return {
            "connection_id": self.connection_id,
            "device_id": self.device_id,
            "client_address": self.client_address,
            "state": self.state.name,
            "priority": self.priority.name,
            "created_time": self.created_time,
            "connected_time": self.connected_time,
            "last_activity_time": self.last_activity_time,
            "last_heartbeat_time": self.last_heartbeat_time,
            "idle_time": self.idle_time,
            "bytes_sent": self.bytes_sent,
            "bytes_received": self.bytes_received,
            "errors_count": self.errors_count,
            "reconnect_attempts": self.reconnect_attempts,
            "has_session": self.session is not None
        }


class SessionManager:
    """Manages connection sessions."""
    
    def __init__(self):
        """Initialize session manager."""
        self.sessions: Dict[str, Session] = {}
        self.connection_sessions: Dict[str, str] = {}  # connection_id -> session_id
        self.cleanup_task: Optional[asyncio.Task] = None
        self.is_running = False
        
        logger.info("Session manager initialized")
    
    async def start(self):
        """Start session manager."""
        self.is_running = True
        self.cleanup_task = asyncio.create_task(self._cleanup_loop())
        logger.info("Session manager started")
    
    async def stop(self):
        """Stop session manager."""
        self.is_running = False
        if self.cleanup_task:
            self.cleanup_task.cancel()
            try:
                await self.cleanup_task
            except asyncio.CancelledError:
                pass
        logger.info("Session manager stopped")
    
    async def create_session(self, connection_id: str, user_id: Optional[str] = None) -> Session:
        """Create new session.
        
        Args:
            connection_id: Associated connection ID
            user_id: Optional user identifier
            
        Returns:
            Created session
        """
        session_id = str(uuid.uuid4())
        session = Session(
            session_id=session_id,
            connection_id=connection_id,
            user_id=user_id
        )
        
        self.sessions[session_id] = session
        self.connection_sessions[connection_id] = session_id
        
        logger.info(f"Session created: {session_id} for connection {connection_id}")
        return session
    
    def get_session(self, session_id: str) -> Optional[Session]:
        """Get session by ID.
        
        Args:
            session_id: Session identifier
            
        Returns:
            Session or None if not found
        """
        session = self.sessions.get(session_id)
        if session and not session.is_expired:
            session.touch()
            return session
        elif session:
            # Session expired, remove it
            self.remove_session(session_id)
        return None
    
    def get_session_by_connection(self, connection_id: str) -> Optional[Session]:
        """Get session by connection ID.
        
        Args:
            connection_id: Connection identifier
            
        Returns:
            Session or None if not found
        """
        session_id = self.connection_sessions.get(connection_id)
        if session_id:
            return self.get_session(session_id)
        return None
    
    def update_session(self, session_id: str, data: Dict[str, Any]) -> bool:
        """Update session data.
        
        Args:
            session_id: Session identifier
            data: Data to update
            
        Returns:
            True if updated successfully
        """
        session = self.get_session(session_id)
        if session:
            session.data.update(data)
            session.touch()
            return True
        return False
    
    def remove_session(self, session_id: str) -> bool:
        """Remove session.
        
        Args:
            session_id: Session identifier
            
        Returns:
            True if removed successfully
        """
        session = self.sessions.pop(session_id, None)
        if session:
            self.connection_sessions.pop(session.connection_id, None)
            logger.info(f"Session removed: {session_id}")
            return True
        return False
    
    def remove_session_by_connection(self, connection_id: str) -> bool:
        """Remove session by connection ID.
        
        Args:
            connection_id: Connection identifier
            
        Returns:
            True if removed successfully
        """
        session_id = self.connection_sessions.get(connection_id)
        if session_id:
            return self.remove_session(session_id)
        return False
    
    async def cleanup_expired_sessions(self) -> int:
        """Clean up expired sessions.
        
        Returns:
            Number of sessions cleaned up
        """
        expired_sessions = []
        current_time = time.time()
        
        for session_id, session in self.sessions.items():
            if session.is_expired:
                expired_sessions.append(session_id)
        
        for session_id in expired_sessions:
            self.remove_session(session_id)
        
        if expired_sessions:
            logger.info(f"Cleaned up {len(expired_sessions)} expired sessions")
        
        return len(expired_sessions)
    
    def get_statistics(self) -> Dict[str, Any]:
        """Get session statistics.
        
        Returns:
            Session statistics
        """
        active_sessions = len([s for s in self.sessions.values() if not s.is_expired])
        authenticated_sessions = len([s for s in self.sessions.values() 
                                    if s.is_authenticated and not s.is_expired])
        
        return {
            "total_sessions": len(self.sessions),
            "active_sessions": active_sessions,
            "authenticated_sessions": authenticated_sessions,
            "expired_sessions": len(self.sessions) - active_sessions
        }
    
    async def _cleanup_loop(self):
        """Background cleanup loop."""
        while self.is_running:
            try:
                await asyncio.sleep(CLEANUP_INTERVAL)
                await self.cleanup_expired_sessions()
                
                # Force garbage collection periodically
                if len(self.sessions) > 100:
                    gc.collect()
                    
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Session cleanup error: {e}")


class ReconnectManager:
    """Manages automatic reconnection for failed connections."""
    
    def __init__(self, connection_manager: 'ConnectionManager'):
        """Initialize reconnect manager.
        
        Args:
            connection_manager: Parent connection manager
        """
        self.connection_manager = connection_manager
        self.pending_reconnects: Dict[str, asyncio.Task] = {}
        self.is_running = False
        
        logger.info("Reconnect manager initialized")
    
    async def start(self):
        """Start reconnect manager."""
        self.is_running = True
        logger.info("Reconnect manager started")
    
    async def stop(self):
        """Stop reconnect manager."""
        self.is_running = False
        
        # Cancel pending reconnects
        for task in self.pending_reconnects.values():
            task.cancel()
        
        # Wait for tasks to complete
        if self.pending_reconnects:
            await asyncio.gather(*self.pending_reconnects.values(), return_exceptions=True)
            self.pending_reconnects.clear()
        
        logger.info("Reconnect manager stopped")
    
    def schedule_reconnect(self, connection: Connection):
        """Schedule reconnection for a connection.
        
        Args:
            connection: Connection to reconnect
        """
        if not self.is_running:
            return
        
        if connection.connection_id in self.pending_reconnects:
            # Already scheduled
            return
        
        if connection.reconnect_attempts >= RECONNECT_ATTEMPTS:
            logger.warning(f"Max reconnect attempts reached for {connection.connection_id}")
            return
        
        connection.state = ConnectionState.RECONNECTING
        connection.reconnect_attempts += 1
        
        # Calculate reconnect delay
        delay = self._calculate_delay(connection)
        
        # Schedule reconnect task
        task = asyncio.create_task(self._reconnect_after_delay(connection, delay))
        self.pending_reconnects[connection.connection_id] = task
        
        logger.info(f"Scheduled reconnect for {connection.connection_id} "
                   f"(attempt {connection.reconnect_attempts}/{RECONNECT_ATTEMPTS}, "
                   f"delay {delay:.1f}s)")
    
    def cancel_reconnect(self, connection_id: str):
        """Cancel scheduled reconnect.
        
        Args:
            connection_id: Connection identifier
        """
        task = self.pending_reconnects.pop(connection_id, None)
        if task:
            task.cancel()
            logger.info(f"Cancelled reconnect for {connection_id}")
    
    def _calculate_delay(self, connection: Connection) -> float:
        """Calculate reconnect delay based on strategy.
        
        Args:
            connection: Connection to calculate delay for
            
        Returns:
            Delay in seconds
        """
        attempt = connection.reconnect_attempts
        
        if connection.reconnect_strategy == ReconnectStrategy.IMMEDIATE:
            return 0.0
        elif connection.reconnect_strategy == ReconnectStrategy.LINEAR_BACKOFF:
            delay = RECONNECT_DELAY * attempt
        elif connection.reconnect_strategy == ReconnectStrategy.EXPONENTIAL_BACKOFF:
            delay = RECONNECT_DELAY * (2 ** (attempt - 1))
        else:
            delay = RECONNECT_DELAY
        
        # Add jitter to avoid thundering herd
        jitter = random.uniform(0, delay * 0.1)
        delay += jitter
        
        # Cap maximum delay
        return min(delay, 60.0)
    
    async def _reconnect_after_delay(self, connection: Connection, delay: float):
        """Perform reconnect after delay.
        
        Args:
            connection: Connection to reconnect
            delay: Delay before reconnect
        """
        try:
            await asyncio.sleep(delay)
            
            if not self.is_running:
                return
            
            connection.last_reconnect_time = time.time()
            
            # Attempt reconnection through connection manager
            success = await self.connection_manager._attempt_reconnect(connection)
            
            if success:
                connection.state = ConnectionState.CONNECTED
                connection.reconnect_attempts = 0  # Reset on success
                logger.info(f"Successfully reconnected {connection.connection_id}")
            else:
                # Schedule another attempt if we haven't exceeded the limit
                if connection.reconnect_attempts < RECONNECT_ATTEMPTS:
                    self.schedule_reconnect(connection)
                else:
                    connection.state = ConnectionState.ERROR
                    logger.error(f"Failed to reconnect {connection.connection_id} "
                               f"after {RECONNECT_ATTEMPTS} attempts")
            
        except asyncio.CancelledError:
            logger.info(f"Reconnect cancelled for {connection.connection_id}")
        except Exception as e:
            logger.error(f"Reconnect error for {connection.connection_id}: {e}")
        finally:
            self.pending_reconnects.pop(connection.connection_id, None)


class ConnectionMonitor:
    """Monitors connection health and performance."""
    
    def __init__(self, connection_manager: 'ConnectionManager'):
        """Initialize connection monitor.
        
        Args:
            connection_manager: Parent connection manager
        """
        self.connection_manager = connection_manager
        self.monitor_task: Optional[asyncio.Task] = None
        self.is_running = False
        
        # Callbacks
        self.on_connection_unhealthy: Optional[Callable[[Connection], None]] = None
        self.on_heartbeat_failure: Optional[Callable[[Connection], None]] = None
        
        logger.info("Connection monitor initialized")
    
    async def start(self):
        """Start connection monitoring."""
        self.is_running = True
        self.monitor_task = asyncio.create_task(self._monitor_loop())
        logger.info("Connection monitor started")
    
    async def stop(self):
        """Stop connection monitoring."""
        self.is_running = False
        if self.monitor_task:
            self.monitor_task.cancel()
            try:
                await self.monitor_task
            except asyncio.CancelledError:
                pass
        logger.info("Connection monitor stopped")
    
    async def check_connection_health(self, connection: Connection) -> bool:
        """Check connection health status.
        
        Args:
            connection: Connection to check
            
        Returns:
            True if connection is healthy
        """
        # Check basic connection state
        if connection.state in [ConnectionState.ERROR, ConnectionState.TERMINATED]:
            return False
        
        # Check heartbeat status
        if connection.heartbeat_failed:
            logger.warning(f"Heartbeat failed for connection {connection.connection_id}")
            if self.on_heartbeat_failure:
                self.on_heartbeat_failure(connection)
            return False
        
        # Check idle time
        if connection.is_idle and connection.state == ConnectionState.ACTIVE:
            logger.debug(f"Connection {connection.connection_id} is idle")
            connection.state = ConnectionState.IDLE
        
        return True
    
    async def send_heartbeat(self, connection: Connection) -> bool:
        """Send heartbeat to connection.
        
        Args:
            connection: Connection to send heartbeat to
            
        Returns:
            True if heartbeat sent successfully
        """
        try:
            # Send heartbeat through connection manager
            success = await self.connection_manager._send_heartbeat(connection)
            
            if success:
                connection.update_heartbeat()
                logger.debug(f"Heartbeat sent to {connection.connection_id}")
            else:
                logger.warning(f"Failed to send heartbeat to {connection.connection_id}")
            
            return success
            
        except Exception as e:
            logger.error(f"Heartbeat error for {connection.connection_id}: {e}")
            return False
    
    async def _monitor_loop(self):
        """Main monitoring loop."""
        while self.is_running:
            try:
                await asyncio.sleep(HEARTBEAT_INTERVAL)
                await self._check_all_connections()
                
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Monitor loop error: {e}")
    
    async def _check_all_connections(self):
        """Check all active connections."""
        unhealthy_connections = []
        heartbeat_tasks = []
        
        for connection in self.connection_manager.get_active_connections():
            # Check health
            if not await self.check_connection_health(connection):
                unhealthy_connections.append(connection)
                continue
            
            # Send heartbeat if needed
            if connection.needs_heartbeat:
                heartbeat_tasks.append(self.send_heartbeat(connection))
        
        # Send heartbeats concurrently
        if heartbeat_tasks:
            await asyncio.gather(*heartbeat_tasks, return_exceptions=True)
        
        # Handle unhealthy connections
        for connection in unhealthy_connections:
            if self.on_connection_unhealthy:
                self.on_connection_unhealthy(connection)


class ConnectionPool:
    """Manages a pool of BLE connections."""
    
    def __init__(self, max_size: int = MAX_CONNECTIONS):
        """Initialize connection pool.
        
        Args:
            max_size: Maximum number of connections in pool
        """
        self.max_size = max_size
        self.connections: Dict[str, Connection] = {}
        self.device_connections: Dict[str, Set[str]] = defaultdict(set)  # device_id -> connection_ids
        self.priority_queue: Dict[ConnectionPriority, deque] = {
            priority: deque() for priority in ConnectionPriority
        }
        
        logger.info(f"Connection pool initialized (max_size: {max_size})")
    
    def add_connection(self, connection: Connection) -> bool:
        """Add connection to pool.
        
        Args:
            connection: Connection to add
            
        Returns:
            True if added successfully
        """
        if len(self.connections) >= self.max_size:
            # Try to remove idle or low priority connections
            if not self._make_room_for_connection(connection):
                logger.warning(f"Connection pool full, cannot add {connection.connection_id}")
                return False
        
        self.connections[connection.connection_id] = connection
        self.device_connections[connection.device_id].add(connection.connection_id)
        self.priority_queue[connection.priority].append(connection.connection_id)
        
        logger.info(f"Added connection to pool: {connection.connection_id} "
                   f"({len(self.connections)}/{self.max_size})")
        return True
    
    def remove_connection(self, connection_id: str) -> Optional[Connection]:
        """Remove connection from pool.
        
        Args:
            connection_id: Connection identifier
            
        Returns:
            Removed connection or None
        """
        connection = self.connections.pop(connection_id, None)
        if connection:
            self.device_connections[connection.device_id].discard(connection_id)
            if not self.device_connections[connection.device_id]:
                del self.device_connections[connection.device_id]
            
            # Remove from priority queue
            try:
                self.priority_queue[connection.priority].remove(connection_id)
            except ValueError:
                pass  # Not in queue
            
            logger.info(f"Removed connection from pool: {connection_id}")
        
        return connection
    
    def get_connection(self, connection_id: str) -> Optional[Connection]:
        """Get connection by ID.
        
        Args:
            connection_id: Connection identifier
            
        Returns:
            Connection or None
        """
        return self.connections.get(connection_id)
    
    def get_connections_by_device(self, device_id: str) -> List[Connection]:
        """Get all connections for a device.
        
        Args:
            device_id: Device identifier
            
        Returns:
            List of connections
        """
        connection_ids = self.device_connections.get(device_id, set())
        return [self.connections[conn_id] for conn_id in connection_ids 
                if conn_id in self.connections]
    
    def get_active_connections(self) -> List[Connection]:
        """Get all active connections.
        
        Returns:
            List of active connections
        """
        return [conn for conn in self.connections.values() 
                if conn.state not in [ConnectionState.ERROR, ConnectionState.TERMINATED]]
    
    def get_idle_connections(self) -> List[Connection]:
        """Get idle connections that can be closed.
        
        Returns:
            List of idle connections
        """
        return [conn for conn in self.connections.values() if conn.is_idle]
    
    def _make_room_for_connection(self, new_connection: Connection) -> bool:
        """Make room for a new connection by removing lower priority ones.
        
        Args:
            new_connection: New connection to make room for
            
        Returns:
            True if room was made
        """
        # First try idle connections
        idle_connections = self.get_idle_connections()
        if idle_connections:
            # Remove the oldest idle connection
            oldest_idle = min(idle_connections, key=lambda c: c.last_activity_time)
            self.remove_connection(oldest_idle.connection_id)
            logger.info(f"Removed idle connection {oldest_idle.connection_id} to make room")
            return True
        
        # Then try lower priority connections
        for priority in [ConnectionPriority.LOW, ConnectionPriority.NORMAL]:
            if priority.value >= new_connection.priority.value:
                break
            
            if self.priority_queue[priority]:
                conn_id = self.priority_queue[priority].popleft()
                if conn_id in self.connections:
                    self.remove_connection(conn_id)
                    logger.info(f"Removed lower priority connection {conn_id} to make room")
                    return True
        
        return False
    
    def get_statistics(self) -> Dict[str, Any]:
        """Get pool statistics.
        
        Returns:
            Pool statistics
        """
        active_count = len(self.get_active_connections())
        idle_count = len(self.get_idle_connections())
        
        state_counts = defaultdict(int)
        priority_counts = defaultdict(int)
        
        for conn in self.connections.values():
            state_counts[conn.state.name] += 1
            priority_counts[conn.priority.name] += 1
        
        return {
            "total_connections": len(self.connections),
            "active_connections": active_count,
            "idle_connections": idle_count,
            "max_connections": self.max_size,
            "utilization_percent": (len(self.connections) / self.max_size) * 100,
            "state_distribution": dict(state_counts),
            "priority_distribution": dict(priority_counts),
            "devices_connected": len(self.device_connections)
        }


class ConnectionManager:
    """Main BLE connection management system."""
    
    def __init__(self, max_connections: int = MAX_CONNECTIONS):
        """Initialize connection manager.
        
        Args:
            max_connections: Maximum concurrent connections
        """
        self.max_connections = max_connections
        
        # Core components
        self.pool = ConnectionPool(max_connections)
        self.session_manager = SessionManager()
        self.reconnect_manager = ReconnectManager(self)
        self.monitor = ConnectionMonitor(self)
        
        # Metrics
        self.metrics = ConnectionMetrics()
        
        # State
        self.is_running = False
        
        # Callbacks
        self.on_connection_established: Optional[Callable[[Connection], None]] = None
        self.on_connection_lost: Optional[Callable[[Connection], None]] = None
        self.on_connection_error: Optional[Callable[[Connection, Exception], None]] = None
        
        # Setup monitor callbacks
        self.monitor.on_connection_unhealthy = self._handle_unhealthy_connection
        self.monitor.on_heartbeat_failure = self._handle_heartbeat_failure
        
        logger.info(f"Connection manager initialized (max_connections: {max_connections})")
    
    async def start(self):
        """Start connection manager."""
        self.is_running = True
        
        # Start sub-components
        await self.session_manager.start()
        await self.reconnect_manager.start()
        await self.monitor.start()
        
        logger.info("Connection manager started")
    
    async def stop(self):
        """Stop connection manager."""
        self.is_running = False
        
        # Disconnect all connections
        await self.disconnect_all()
        
        # Stop sub-components
        await self.monitor.stop()
        await self.reconnect_manager.stop()
        await self.session_manager.stop()
        
        logger.info("Connection manager stopped")
    
    async def accept_connection(self, device_id: str, client_address: str,
                              priority: ConnectionPriority = ConnectionPriority.NORMAL) -> Optional[Connection]:
        """Accept new BLE connection.
        
        Args:
            device_id: BLE device identifier
            client_address: Client MAC address
            priority: Connection priority
            
        Returns:
            Connection object if accepted, None otherwise
        """
        if not self.is_running:
            logger.warning("Connection manager not running, rejecting connection")
            return None
        
        # Generate connection ID
        connection_id = str(uuid.uuid4())
        
        # Create connection object
        connection = Connection(connection_id, device_id, client_address, priority)
        
        # Add to pool
        if not self.pool.add_connection(connection):
            logger.warning(f"Failed to add connection to pool: {connection_id}")
            self.metrics.failed_connections += 1
            return None
        
        # Create session
        session = await self.session_manager.create_session(connection_id)
        connection.session = session
        
        # Update connection state
        connection.state = ConnectionState.CONNECTED
        connection.connected_time = time.time()
        
        # Update metrics
        self.metrics.total_connections += 1
        self.metrics.successful_connections += 1
        self.metrics.active_connections += 1
        self.metrics.sessions_created += 1
        
        # Trigger callback
        if self.on_connection_established:
            self.on_connection_established(connection)
        
        logger.info(f"Connection accepted: {connection_id} from {client_address}")
        return connection
    
    async def disconnect_connection(self, connection_id: str, reason: str = "Manual disconnect"):
        """Disconnect a specific connection.
        
        Args:
            connection_id: Connection identifier
            reason: Disconnection reason
        """
        connection = self.pool.get_connection(connection_id)
        if not connection:
            logger.warning(f"Connection not found for disconnect: {connection_id}")
            return
        
        # Update state
        connection.state = ConnectionState.DISCONNECTED
        
        # Cancel any pending reconnects
        self.reconnect_manager.cancel_reconnect(connection_id)
        
        # Remove session
        self.session_manager.remove_session_by_connection(connection_id)
        
        # Remove from pool
        self.pool.remove_connection(connection_id)
        
        # Update metrics
        self.metrics.active_connections = max(0, self.metrics.active_connections - 1)
        
        # Trigger callback
        if self.on_connection_lost:
            self.on_connection_lost(connection)
        
        logger.info(f"Connection disconnected: {connection_id} ({reason})")
    
    async def disconnect_all(self):
        """Disconnect all active connections."""
        connection_ids = list(self.pool.connections.keys())
        
        for connection_id in connection_ids:
            await self.disconnect_connection(connection_id, "Manager shutdown")
        
        logger.info(f"Disconnected {len(connection_ids)} connections")
    
    def get_connection(self, connection_id: str) -> Optional[Connection]:
        """Get connection by ID.
        
        Args:
            connection_id: Connection identifier
            
        Returns:
            Connection or None
        """
        return self.pool.get_connection(connection_id)
    
    def get_active_connections(self) -> List[Connection]:
        """Get all active connections.
        
        Returns:
            List of active connections
        """
        return self.pool.get_active_connections()
    
    def get_connection_by_device(self, device_id: str) -> Optional[Connection]:
        """Get first connection for a device.
        
        Args:
            device_id: Device identifier
            
        Returns:
            Connection or None
        """
        connections = self.pool.get_connections_by_device(device_id)
        return connections[0] if connections else None
    
    async def send_data(self, connection_id: str, data: bytes) -> bool:
        """Send data to a connection.
        
        Args:
            connection_id: Connection identifier
            data: Data to send
            
        Returns:
            True if sent successfully
        """
        connection = self.pool.get_connection(connection_id)
        if not connection:
            logger.warning(f"Connection not found for send: {connection_id}")
            return False
        
        if connection.state not in [ConnectionState.CONNECTED, ConnectionState.AUTHENTICATED, ConnectionState.ACTIVE]:
            logger.warning(f"Connection not ready for send: {connection_id} (state: {connection.state})")
            return False
        
        try:
            # This would integrate with actual BLE sending mechanism
            # For now, we simulate the send operation
            await asyncio.sleep(0.001)  # Simulate send latency
            
            # Update connection stats
            connection.bytes_sent += len(data)
            connection.messages_sent += 1
            connection.update_activity()
            
            # Update metrics
            self.metrics.data_sent += len(data)
            
            logger.debug(f"Sent {len(data)} bytes to {connection_id}")
            return True
            
        except Exception as e:
            connection.errors_count += 1
            self.metrics.connection_errors += 1
            
            if self.on_connection_error:
                self.on_connection_error(connection, e)
            
            logger.error(f"Failed to send data to {connection_id}: {e}")
            return False
    
    async def _send_heartbeat(self, connection: Connection) -> bool:
        """Send heartbeat to connection (internal method).
        
        Args:
            connection: Connection to send heartbeat to
            
        Returns:
            True if successful
        """
        heartbeat_data = b"HEARTBEAT"
        return await self.send_data(connection.connection_id, heartbeat_data)
    
    async def _attempt_reconnect(self, connection: Connection) -> bool:
        """Attempt to reconnect a connection (internal method).
        
        Args:
            connection: Connection to reconnect
            
        Returns:
            True if reconnection successful
        """
        try:
            # Simulate reconnection attempt
            await asyncio.sleep(0.1)
            
            # Update metrics
            self.metrics.reconnection_attempts += 1
            
            # Simulate success rate (90%)
            success = random.random() < 0.9
            
            if success:
                connection.state = ConnectionState.CONNECTED
                connection.connected_time = time.time()
                self.metrics.successful_reconnections += 1
            
            return success
            
        except Exception as e:
            logger.error(f"Reconnection attempt failed for {connection.connection_id}: {e}")
            return False
    
    def _handle_unhealthy_connection(self, connection: Connection):
        """Handle unhealthy connection.
        
        Args:
            connection: Unhealthy connection
        """
        logger.warning(f"Connection unhealthy: {connection.connection_id}")
        
        if connection.state == ConnectionState.ERROR:
            # Schedule reconnection
            self.reconnect_manager.schedule_reconnect(connection)
        elif connection.is_idle:
            # Mark as idle
            connection.state = ConnectionState.IDLE
    
    def _handle_heartbeat_failure(self, connection: Connection):
        """Handle heartbeat failure.
        
        Args:
            connection: Connection with heartbeat failure
        """
        logger.warning(f"Heartbeat failure: {connection.connection_id}")
        
        connection.state = ConnectionState.ERROR
        self.metrics.heartbeat_failures += 1
        
        # Schedule reconnection
        self.reconnect_manager.schedule_reconnect(connection)
    
    def get_statistics(self) -> Dict[str, Any]:
        """Get comprehensive connection statistics.
        
        Returns:
            Statistics dictionary
        """
        pool_stats = self.pool.get_statistics()
        session_stats = self.session_manager.get_statistics()
        
        # Calculate response time (simulated)
        active_connections = self.pool.get_active_connections()
        if active_connections:
            response_times = [0.05 + random.uniform(0, 0.1) for _ in active_connections]
            avg_response_time = sum(response_times) / len(response_times)
        else:
            avg_response_time = 0.0
        
        self.metrics.average_response_time = avg_response_time
        self.metrics.active_connections = pool_stats["active_connections"]
        
        return {
            "manager": {
                "is_running": self.is_running,
                "max_connections": self.max_connections
            },
            "pool": pool_stats,
            "sessions": session_stats,
            "metrics": {
                "active_connections": self.metrics.active_connections,
                "total_connections": self.metrics.total_connections,
                "connection_success_rate": self.metrics.connection_success_rate,
                "reconnection_success_rate": self.metrics.reconnection_success_rate,
                "average_response_time": self.metrics.average_response_time,
                "data_sent": self.metrics.data_sent,
                "data_received": self.metrics.data_received,
                "connection_errors": self.metrics.connection_errors,
                "heartbeat_failures": self.metrics.heartbeat_failures
            }
        }
    
    async def cleanup(self):
        """Clean up connection manager resources."""
        await self.stop()
        logger.info("Connection manager cleanup completed")


# Example usage and testing
if __name__ == "__main__":
    import asyncio
    
    async def test_connection_manager():
        """Test connection manager functionality."""
        # Create connection manager
        manager = ConnectionManager(max_connections=5)
        
        # Start manager
        await manager.start()
        
        try:
            # Accept some connections
            connections = []
            for i in range(3):
                device_id = f"device_{i}"
                address = f"AA:BB:CC:DD:EE:{i:02d}"
                
                connection = await manager.accept_connection(
                    device_id, address, 
                    ConnectionPriority.NORMAL if i < 2 else ConnectionPriority.HIGH
                )
                
                if connection:
                    connections.append(connection)
                    print(f"Connected: {connection.connection_id}")
            
            # Send some data
            for connection in connections:
                success = await manager.send_data(connection.connection_id, b"Hello BLE!")
                print(f"Send to {connection.connection_id}: {'success' if success else 'failed'}")
            
            # Wait a bit to let monitoring run
            await asyncio.sleep(5)
            
            # Get statistics
            stats = manager.get_statistics()
            print("\nConnection Statistics:")
            print(f"  Active connections: {stats['pool']['active_connections']}")
            print(f"  Total connections: {stats['metrics']['total_connections']}")
            print(f"  Success rate: {stats['metrics']['connection_success_rate']:.2%}")
            print(f"  Data sent: {stats['metrics']['data_sent']} bytes")
            
            # Test disconnection
            if connections:
                await manager.disconnect_connection(connections[0].connection_id)
                print(f"Disconnected: {connections[0].connection_id}")
            
        finally:
            # Clean up
            await manager.cleanup()
    
    # Run test
    asyncio.run(test_connection_manager())